Skip to content

feat(bulma): @paper/bulma — Bulma compat design system (Tier 1) - #760

Open
johnleider wants to merge 19 commits into
devfrom
feat/paper-bulma
Open

feat(bulma): @paper/bulma — Bulma compat design system (Tier 1)#760
johnleider wants to merge 19 commits into
devfrom
feat/paper-bulma

Conversation

@johnleider

@johnleider johnleider commented Aug 1, 2026

Copy link
Copy Markdown
Member

What

First compat-class design system: components render real Bulma 1.0.4 markup and classes against the user's own bulma.css, while @vuetify/v0 supplies all behavior, focus management, and accessibility. Bulma ships zero JavaScript by design — its docs tell you to bring your own JS for modal, dropdown, navbar burger, and dismissals. This package is that JS.

Sibling effort: J-Sek/v0-daisyui (daisyfy). The compat-class addendum to DESIGN_SYSTEMS.md is deliberately deferred — it gets extracted from both packages once Tier 1 experience settles.

Contents

  • 22 Tier-1 components (Bu* prefix; B* reserved for a future Bootstrap compat): BuModal (content + card variants, hand-rolled focus trap), BuDropdown, BuTabs/BuTab/BuTabPanel, BuNavbar, BuMessage, BuNotification, BuPagination, BuBreadcrumb, BuPanel, BuMenu, and the form set — BuField/BuControl/BuLabel/BuHelp/BuInput/BuTextarea/BuSelect/BuCheckbox/BuRadio/BuFile.
  • Conformance harness (harness/): fixtures captured from bulma.io's documented markup; conform() diffs rendered DOM structure, classes, attributes, and text against them with a strict tolerance charter (only additive aria-*/data-*/role/tabindex/id allowed). Built repo-generic so later compat targets reuse it.
  • axe-core a11y sweep over every component (open states included), mirroring packages/0's sweep.
  • SPEC.md declares the three compat-class deviations from DESIGN_SYSTEMS.md rulings (no owned token namespace; upstream is-* state classes; native form controls where Bulma's CSS demands them) and Known Limitations, each with its v0-core follow-up.

v0-core follow-ups surfaced (to be filed as issues)

  • No useFocusTrap composable (BuModal hand-rolls containment)
  • DialogContent stack ticket can't opt out of scrim (global <Scrim> double-backdrops)
  • BreadcrumbsRoot hides overflow items even with no Ellipsis registered
  • TabsItem renderless mode registers no element → focus-follow no-op
  • InputControl mergeProps assigns undefined keys (wrapper can't force readonly); export parseValidateOn from the Input barrel

Notes

  • bulma@^1.0 is an optional peer — zero-config consumers load Bulma off-CDN. No stylesheet artifact ships.
  • 83 browser tests green (bulma:browser project); package + root typecheck, lint, knip/sherif clean.
  • No changeset: @paper/* first publish requires a manual scope bootstrap; versioning starts at 0.1.0 on that event.

Spec approved 2026-07-31 (compat direction, @paper/bulma in-monorepo, Bu* prefix).

Docs (added after review rounds)

A Design Systems docs section with the Bulma pilot: landing + BuModal/BuDropdown pages. Examples run in iframed sandboxes so Bulma's global CSS never touches the docs chrome — frames follow the docs theme live, and modals promote the frame to a transparent full-viewport layer (inline example box pinned in place) so overlays cover the page. Each page pairs the verbatim conformance fixture with the component usage that renders it and links the upstream bulma.io docs. Region slots were also reworked into express part components (BuModalHead/BuModalTitle/…, no named slots) per review.

Components render real Bulma 1.0.4 markup and classes against the user's
own bulma.css while @vuetify/v0 supplies behavior, focus management, and
accessibility. Bulma ships zero JavaScript by design; this package is the
JS its docs tell you to bring.

- 22 Tier-1 components (Bu* prefix): modal, dropdown, tabs, navbar,
  message, notification, pagination, breadcrumb, panel, menu, and the
  full form set (field/control/label/help/input/textarea/select/
  checkbox/radio/file)
- Conformance harness: fixtures captured from bulma.io's documented
  markup, DOM/class diffing with a strict additive-attr tolerance
  charter, plus an axe-core a11y sweep over every component
- SPEC.md declares the compat-class deviations from DESIGN_SYSTEMS.md
  rulings and known Tier-1 limitations, each with its v0-core follow-up
- No stylesheet artifact and no owned token namespace: bulma@^1.0 is an
  optional peer; the user's CSS is the styling source
@johnleider johnleider added this to the v1.1.0 milestone Aug 1, 2026
@johnleider johnleider added the enhancement New feature or request label Aug 1, 2026
@johnleider johnleider self-assigned this Aug 1, 2026
BuInput's internal root is renderless and subtree-scoped, so a sibling
BuHelp silently renders an empty .help while the input still shows
is-danger — deceptively half-wired. Dev-only watchEffect warning plus a
SPEC.md known-limitation entry documenting the InputRoot composition.
No named slots in the package: every named region is now an express part
component named after Bulma's own part class, composed in userland exactly
as the markup nests, reading parent state through provide/inject.

New parts: BuModalCard, BuModalContent, BuModalHead, BuModalTitle,
BuModalBody, BuModalFoot, BuModalClose, BuMessageHeader, BuMessageBody,
BuDropdownTrigger, BuDropdownMenu, BuNavbarBrand, BuNavbarMenu, BuTabList,
BuFieldLabel, BuFieldBody, BuPanelHeading, BuPanelTabs, BuPanelTab,
BuPanelBlock, BuPanelIcon.

Text-only slots become props (BuPagination previous/next, BuFile icon);
BuField's label slot is deleted in favour of composing BuLabel.
Landing page + BuModal/BuDropdown pages under a new Design Systems nav
section (clickable parent node, Guide-style). Examples render inside
iframed sandboxes (separate chrome-less vite entry) so Bulma's global
CSS stays isolated from the docs chrome: frames follow the docs theme
live via postMessage, and overlay components promote the frame to a
transparent full-viewport layer while pinning the inline example box in
place (0px trigger drift), restoring on close. Each page pairs the
verbatim conformance fixture markup with the component usage that
renders it, and links the upstream bulma.io documentation.
Recipes promoted into four iframed examples (hoverable, alignment,
menu content, arbitrary content) each covering one concept; Accessibility
gains a Dismissal table and the subtree-bound Escape rationale; role=menu
policy gets its own Menu semantics section. Sandbox now measures on
pointer events so CSS-only hover reveals resize their frame.
…motion

Generalizes promotion from position:fixed detection to any floating
content escaping the inline box. Iframes paint an opaque canvas even
with transparent documents, so the promoted frame is clipped via
clip-path to exactly the regions the sandbox reports painting (pinned
box + escaping surfaces) — this also fixes the committed modal path,
whose backdrop was dimming the frame's white canvas rather than the
page. Clipped regions aren't hit-testable, so the parent replays
outside pointerdowns for v0's click-outside; the pin re-syncs on
scroll while promoted. Promotion engages only when content actually
escapes (the alignment example's is-up menu fits its box and never
promotes); hover promotion proved flicker-free with deferred pointer
reads and an in-flight gate.
… promote

The ancestor z-index lift was tracked per example instance while
lifting the same shared ancestors, so overlapping promotions restored
each other's lifted values and left page content permanently raised.
Promotion is now a page-level singleton: one owner token, one shared
lift record, one release() used by close, displacement, and teardown;
a second promoter displaces the first with full restore. Hover-revealed
content latches to frame growth for the whole reveal — hover promotion
churned the shared lift and the growth resize re-promoted on rebound.
…y covers it

The sandbox classifies each promotion: overlay (viewport-laid-out
surface — modal + backdrop) lifts ancestor stacking contexts to cover
chrome as before; float (in-flow content spilling out — dropdown menus)
never lifts, so the sticky app bar occludes it on scroll exactly like
surrounding page content. Pin, clip, singleton ownership, dismissal
bridge, and scroll re-sync stay shared; a float escalates to overlay
on the next rects message if a modal opens within it.
…gions

Region rects were measured before pin() ran, so they described the
sandbox's frame-local layout and the parent clipped the promoted frame
to a box at the viewport corner — the visible flash, intermittent only
by how many frames it survived. Order is now pin, re-measure, post;
promotion is refused when no box rect is cached (falls back to growth).
The close path had the mirror bug (unpin before demote) — it now holds
the pin until the parent's demotion ack, with a 300ms safety. Verified
frame-granular: 1,300+ sampled promoted frames across both tiers and
themes at 1x and 4x CPU throttle, zero flashed or unclipped frames.
…ristics

The hover latch raced the observers — whichever signal classified a
reveal first won, so is-hoverable menus nondeterministically grew or
promoted. Classification is now a fact read fresh at measurement time:
viewport-laid-out surface → overlay; escaping content owned by an
is-active ancestor (component-set state only) → float; anything else,
including pure-CSS :hover reveals → grow. The pointer listener remains
solely as a change-detector (CSS reveals fire no observer); the
growing latch and hover-threading are deleted. Also fixes the growth
path reading the now-empty promotion rects instead of the escaping
surfaces.
Reuses Bulma's own markup — .field.has-addons > .control > button.button /
input.input — so the composite needs zero custom CSS; verified in-browser that
upstream's addon radii, flex, and is-expanded rules engage on it unchanged.
Ships the first SELF-AUTHORED fixture (Bulma documents no number input): its
header states the provenance and traces every class to another fixture block or
a bulma.css selector, so only the arrangement is ours and drift still fails.
Parts are BuNumberFieldDecrement / BuNumberFieldInput / BuNumberFieldIncrement,
each rendering its own structural .control.
First composite (gap-filler) page: no upstream bulma.io component page
exists, so 'The markup you know' becomes 'The markup it composes' — the
Bulma tab shows the self-authored fixture whose provenance traces every
class to an upstream fixture block or bulma.css selector, prose states
what conformance can and cannot claim for a composed contract, and the
upstream link targets the has-addons form docs the composition derives
from. Index Components table gains a 'None — composed' taxonomy row.
…boxes

White flash on dark theme: the sandbox HTML head now sets data-theme
from the query synchronously with critical background colors inline
(no dependence on CDN CSS or the app bundle), and the parent aligns
the iframe's color-scheme with the docs theme — a scheme mismatch
forces an opaque white canvas even before load.

Height jumps: measurements posted before the sandbox's ready handshake
(the empty-document size) are ignored; the placeholder reserves the
last-measured height per example from sessionStorage so warm loads
have zero shifts and cold loads settle once. The reserve yields to
every post-ready measurement in both directions, and each applied
measurement refreshes the cache.
Keep Bulma iframe sandbox for CSS-reset systems; Emerald stays inline.
Fill remaining Bu* pages (Composed on v0, Reference, examples).
Systems previews: light/dark only, shared code-group expand,
iframe first-paint pulse, no-resize landing example, glass-surface
safelist, AppNav hide transform without ltr:.
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Changeset found — this change will be included in the next release. Thanks!

@johnleider
johnleider marked this pull request as ready for review August 21, 2026 21:39
`!v0:browser` still matched `bulma:browser`, so the unit job launched
Chromium without installing it. Exclude every `*:browser` project from
unit and run them all in the browser job.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant